昨天我們快速地展示了 FastAPI 自動生成 API 文件的功能,接下來就會開始介紹怎麼進一步地設定裡面的內容,並聊聊兩個在實務開發上遇到的需求
P.S. 這部分官網寫的比較分散一些
在昨天的文章有提到有兩種風格的 API 文件,分別是 Swagger UI 和 ReDoc,他們都是可以產生互動式 API 文件的免費開源工具。兩者的最大差別在排版邏輯,Swagger UI 是一整排列下來,可以再手動展開看對應的說明;而 ReDoc 則是有三個欄位 (畫面太窄的話右邊兩個會合併成一個),左邊是分類,中間是說明,右邊是程式碼範例。這兩種文件都有人在用,但我個人感覺比較常看到 Swagger UI 的介紹,平時也是比較習慣使用 Swagger UI 來快速確認 API 清單,因此,後續介紹會以 Swagger UI (也就是 /docs 上的版本) 為主。
大家可以參考這篇 2023 年的文章,它介紹了好幾個生成 API 文件的工具
Tag 可以加在 include_router
內
from fastapi import APIRouter
from routers.api_v1.endpoints.user import router as user_router
from routers.api_v1.endpoints.data import router as data_router
router = APIRouter()
router.include_router(user_router, prefix="/user", tags=["user"])
router.include_router(data_router, prefix="/data", tags=["data"])
也可以加在各個 API 內
@router.get("/{user_id}", tags=["ithome"])
async def root(user_id):
return {"message": f"Hello user {user_id}"}
API 文件就會變成這樣
但我比較喜歡一個 API 只用一個 tag,因為這樣才不會看起來有過多的 API
相信大家應該也有發現,每個 API 後面都有一個 Root
,這是因為我每個 API 的 function 名稱都用 root
(複製貼上的XD),只要把 function 名稱改掉就可以了。
# data.py
@router.get("")
async def get_data():
return {"message": "Hello World"}
此外,也可以考慮直接設定 summary
和 description
,summary
會直接取代原本用 function 名稱的說明
# data.py
@router.get("", summary="summmary", description="This is a short description")
async def get_data():
return {"message": "Hello World"}
如果想要寫更長的說明,可以改成用多行註解的方式,同時,也可以使用 Markdown
# data.py
@router.get("", summary="summmary")
async def get_data():
"""
# Markdwon
## Hello World
Cool!
- I can write markdwon message here
- It's **awesome**
"""
return {"message": "Hello World"}
有時候,我們會想要停用舊的 API,但不希望它直接從文件上消失,這時候可以考慮使用 deprecated
參數,把它設為 True
即可
# data.py
@router.post("", deprecated=True)
async def root():
return {"message": "Hello World"}
API 文件上方的介紹也可以修改
預設的樣子是這樣
經過這樣調整設定後
# main.py
description = """
# 詳細說明
這邊也支援 **Markdwon**
"""
app = FastAPI(
title="酷酷的文件標題",
version="1.2.3",
summary="這只是 Demo 用的",
description=description
)
就變成這樣了
在某些情況下,我們不希望後端提供 API 文件 (資安上的考量),此時可以把 openapi_url
設成空字串,就可以讓 /docs
和 /redoc
出現 API 文件的功能失效,只回傳 {"detail":"Not Found"}
# main.py
app = FastAPI(openapi_url="")
上面介紹了寫 API 文件時,常用的設定方法,但有時候,我們會有一些比較不一樣的需求...
乍聽之下這個要求很奇怪,為什麼需要有兩份?
但仔細想想,也不是不可能,例如:我們想要將兩個不同版本的 API 文件分開來
這時候就需要引入 Sub Application 的概念了
我們來看這段程式碼
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/app")
def read_main():
return {"message": "Hello World from main app"}
subapi = FastAPI()
@subapi.get("/sub")
def read_sub():
return {"message": "Hello World from sub API"}
app.mount("/subapi", subapi)
透過把 subapi
mount 在 app
上,我們就可以同時有多個 API 文件
這個做法就是讓每個版本的 API 都是一個 sub application,之後管理上也會比較方便
API 版本管理之後有機會再來更進一步討論XD
官方說明
在某些情況下,我們會在一個網域同時架設多個服務,此時我們會使用 Nginx、Traefik、Caddy 這類的工具,幫助我們進行反向代理 (Reverse proxy),讓我們可以根據路由來選擇後面對應的服務。
考量到篇幅問題,以及避免篇題,對於 Proxy Server 的介紹和設定,這邊就不著墨了...
一般的 API 沒什麼問題,但 API 文件卻會發生錯誤,這是因為 API 文件預設是去 /openapi.json
找檔案,而這個卻沒有被 Proxy server 處理到,自然就不會導到 FastAPI 後端了。
即使多設定規則,讓
/openapi.json
也導到 FastAPI 後端,你會發現後續還有其他檔案要處理,非常麻煩XD
因此,其中一個簡單粗暴的作法,就是把所有服務預設都導向 FastAPI 後端 (不加額外路由),其他服務則需要添加額外路由,並用該路由判斷要使用哪個服務。
但這個方法也僅限於同時只有一個 FastAPI 後端,如果有兩個不同服務 (或是專案) 都是使用 FastAPI,又要同時放在同一個網域下,就會出錯。
當然,這種情況也有一個粗暴的解法,那就是使用子網域。讓 Proxy Server 可以根據子網域來判斷該進哪一個服務。
那有沒有在不使用子網域,但同時有兩個 FastAPI 後端的解決辦法?
又或者是,有沒有把 FastAPI 放在某個路由下,卻又能讓 API 文件正常運作的方法?
有,那就是要設定 root_path
讓我們直接來 Demo 吧!
這邊我們用 Caddy 建立一個 reverse proxy,以下是 Caddyfile 內容
:1234 {
handle_path /ithome/* {
reverse_proxy /* localhost:8000
}
}
上面這段的意思是,在
1234
port 遇到/ithome
就把請求轉到8000
port,並把路徑的/ithome/
取代成/
(也就是把/ithome
去掉)
此時不論是 http://localhost:8000/api/v1/data
和 http://localhost:1234/ithome/api/v1/data
都可以正確地觸發到 API,但是只有 http://localhost:8000/docs
正常顯示 API 文件,如下圖
接著我們在 main.py
加上 root_path
設定
app = FastAPI(root_path="/ithome")
接著再測試一次
最後成功改成讓 http://localhost:1234/ithome/docs
顯示 API 文件了
今天我們介紹了
最後面這段有關 Proxy Server 的內容其實是可以展開成獨立一篇,但考量之後決定只著重在 API 文件設定的部分,未來寫到 FastAPI 部屬的時候,再看看要不要寫完整一點
有問題歡迎留言討論~